package com.tiantian.subject.service.impl;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.crypto.SecureUtil;
import com.tiantian.common.core.exception.BusinessException;
import com.tiantian.common.satoken.utils.LoginHelper;
import com.tiantian.subject.domain.dto.MergeChunksDto;
import com.tiantian.subject.domain.entity.UserFiles;
import com.tiantian.subject.mapper.UserFilesMapper;
import com.tiantian.subject.service.QuestionBankFileUploadService;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

/**
 * @author tiantian
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class QuestionBankFileUploadServiceImpl implements QuestionBankFileUploadService {

    // 资源文件桶名称 todo 上线时修改
    @Value("${jx-minio.baseBucketName}")
    private String questionBankBucket;
    // 临时分片文件名称
    @Value("${jx-minio.tempChunkBucket}")
    private String tempChunkBucket;
    // 分隔符
    private static final String SEPARATOR = "/";
    // 分块目录
    private static final String CHUCK_DIR = "/chuck/";

    private final MinioClient minioClient;
    private final UserFilesMapper userFilesMapper;

    @Override
    public boolean uploadSmallFile(MultipartFile file, String curPath) {
        InputStream is;
        String md5 = null;
        Long userId = null;
        try {
            is = file.getInputStream();
            String filename = file.getOriginalFilename();
            minioClient.putObject(PutObjectArgs.builder().object(curPath + filename).bucket(questionBankBucket).stream(is, file.getSize(), -1).contentType(file.getContentType()).build());
            userId = LoginHelper.getUserId();
            md5 = SecureUtil.md5(is);
            // 将文件信息保存到数据库中
            userFilesMapper.insert(IdUtil.getSnowflakeNextId(), md5, userId);
        } catch (Exception e) {
            log.error("文件上传失败,md5: {}, userId: {}, fileName:{} error:{}", md5, userId, file.getOriginalFilename(), e.getMessage());
            e.printStackTrace();
            throw new BusinessException("文件上传失败: " + e.getMessage());
        }
        return true;
    }

    /**
     * 检查文件是否存在
     */
    @Override
    public boolean checkFileExist(String fileMd5) {
        // 数据库中查询文件信息
        UserFiles userFiles = userFilesMapper.selectByMD5(fileMd5);
        return ObjUtil.isNotNull(userFiles);
    }

    /**
     * 检查分块是否存在
     */
    @Override
    public boolean checkChunkExist(String fileMd5, int chunkIndex) {
        // 得到分块文件的路径
        String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunkIndex;
        boolean exists = false;
        try {
            minioClient.getObject(GetObjectArgs.builder().bucket(tempChunkBucket).object(chunkFilePath).build());
            exists = true;
        } catch (Exception ignored) {
        }
        return exists;
    }

    /**
     * 上传分块
     */
    @Override
    public boolean uploadChunk(MultipartFile file, String fileMd5, int chunk) {
        // 检查分片是否已经上传
        if (checkChunkExist(fileMd5, chunk)) {
            return true;
        }
        try {
            // 创建一个临时文件
            File tempFile = File.createTempFile("minio", ".temp");
            file.transferTo(tempFile);
            String localChunkFilePath = tempFile.getAbsolutePath();
            // 得到分块文件的路径
            String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunk;
            // 将文件存储至MinIO
            minioClient.uploadObject(UploadObjectArgs.builder().filename(localChunkFilePath).object(chunkFilePath).bucket(tempChunkBucket).build());
        } catch (Exception e) {
            log.error("error: {}, fileMd5:{},", e.getMessage(), fileMd5);
            throw new BusinessException("文件上传失败" + e.getMessage());
        }
        return true;
    }

    /**
     * 合并文件
     */
    @Override
    public boolean mergeChunks(MergeChunksDto mergeChunksDto) {
        String fileMd5 = mergeChunksDto.getFileMd5();
        int chunkTotal = mergeChunksDto.getChunkTotal();
        String fileName = mergeChunksDto.getFileName();
        String curPath = mergeChunksDto.getCurPath();

        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        // 合并文件路径
        String mergeFilePath = getPostMergerFilePath(curPath, fileName);
        // 组成将分块文件路径组成 List<ComposeSource>
        List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i)
                .limit(chunkTotal)
                .map(i -> ComposeSource.builder().bucket(tempChunkBucket).object(chunkFileFolderPath.concat(Integer.toString(i))).build())
                .toList();
        try {
            // 合并文件
            minioClient.composeObject(ComposeObjectArgs.builder().bucket(questionBankBucket).object(mergeFilePath).sources(sourceObjectList).build());
            log.debug("合并文件成功: {}", mergeFilePath);
        } catch (Exception e) {
            log.debug("合并文件失败, fileMd5: {},异常: {}", fileMd5, e.getMessage());
            throw new BusinessException("文件上传失败" + e.getMessage());
        }
        String md5HexFromMinIO;
        try {
            InputStream is = minioClient.getObject(GetObjectArgs.builder().bucket(questionBankBucket).object(mergeFilePath).build());
            md5HexFromMinIO = SecureUtil.md5(is);
            // 比较md5值，不一致则说明文件不完整
            if (!fileMd5.equals(md5HexFromMinIO)) {
                log.debug("下载合并文件失败,mergeFilePath: {}", mergeFilePath);
                return false;
            }
        } catch (Exception e) {
            log.error("校验文件失败,fileMd5:{},mergeFilePath:{},异常:{},{}", fileMd5, mergeFilePath, e.getMessage(), e);
            e.printStackTrace();
            return false;
        }
        // 将文件信息保存到数据库中
        userFilesMapper.insert(IdUtil.getSnowflakeNextId(), md5HexFromMinIO, LoginHelper.getUserId());
        // 清除分块文件
        clearChunkFiles(chunkFileFolderPath, chunkTotal);
        return true;
    }

    /**
     * 清除分块文件
     *
     * @param chunkFileFolderPath 分块文件路径
     * @param chunkTotal          分块文件总数
     */
    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {
        try {
            List<DeleteObject> deleteObjects = IntStream.range(0, chunkTotal)
                    .mapToObj(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
                    .collect(Collectors.toList());
            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(tempChunkBucket).objects(deleteObjects).build();
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
            results.forEach(r -> {
                try {
                    DeleteError deleteError = r.get();
                    if (deleteError != null) {
                        log.error("清除分块文件失败, objectName: {}", deleteError.objectName());
                    }
                } catch (Exception e) {
                    log.error("处理删除结果时发生异常:{}", e.getMessage());
                }
            });
        } catch (Exception e) {
            log.error("清除分块文件失败, chunkFileFolderPath: {}", chunkFileFolderPath, e);
        }
    }

    /**
     * 得到块文件所属目录路径
     */
    private String getChunkFileFolderPath(String fileMd5) {
        return SEPARATOR + fileMd5.charAt(0) + SEPARATOR + fileMd5.charAt(1) + SEPARATOR + fileMd5 + CHUCK_DIR;
    }

    /**
     * 得到合并后的文件的地址
     */
    private String getPostMergerFilePath(String curPath, String fileName) {
        return curPath.concat(fileName);
    }


}
